Skip to content

Make L2Denorm an array encoding instead of a scalar function - #9138

Open
connortsui20 wants to merge 1 commit into
developfrom
ct/l2-denorm-encoding
Open

Make L2Denorm an array encoding instead of a scalar function#9138
connortsui20 wants to merge 1 commit into
developfrom
ct/l2-denorm-encoding

Conversation

@connortsui20

@connortsui20 connortsui20 commented Aug 2, 2026

Copy link
Copy Markdown
Member

Rationale for this change

(semi-related) Tracking Issue: #9129

I have a new proposed idiom (cc @gatesn @joseph-isaacs @robert3005): encodings may hold invariants over their children, scalar functions may not.

I think that a scalar function should just be an operation over any well-typed input. Our encoding, however, can "own" their physical decomposition, so they get validation in their constructors and can enforce whatever invariants it needs to between its children.

FoR is the one edge case that I can think of? Its child is an offset from the reference, which is an invariant, but nothing enforces it and encoded + reference would work as a plain scalar function over any integer column.

Anyways, L2Denorm was always a very strange scalar function, as I had to match against it if I ever wanted to guarantee that a vector array was normalized, and it was just very awkward. This PR changes L2Denorm into an encoding instead of a scalar function.

I am also happy to change the name to something else (maybe just "Normalized"?).

What changes are included in this PR?

The unfortunate thing about this is that a lot of mechanical things have to be moved, but the slightly-hairy logic does not. So the large diff here is not a real.

L2Denorm becomes a VTable in vortex-tensor/src/encodings/l2_denorm/ with slots normalized and norms.

  • try_new scans for the exact invariants, try_new_trusted skips the scan for lossy normalized children. Neither is unsafe, since breaking the contract gives wrong answers rather than undefined behavior.
  • Slice and filter push down into both children. Filter is new: the generic ScalarFnArray rule only fires when at most one child is non-constant.
  • L2DenormScheme cascades its children like TemporalScheme, which removes both HACK TO SUPPORT L2 DENORMALIZATION cases from CascadingCompressor.

What APIs are changed? Are there any user-facing changes?

Details
Before After
scalar_fns::l2_denorm::* encodings::l2_denorm::*
L2Denorm::try_new_array(n, s, ctx) L2Denorm::try_new(n, s, ctx)
unsafe L2Denorm::new_array_unchecked(n, s) L2Denorm::try_new_trusted(n, s)
L2Denorm::new() removed
session.scalar_fns().register(L2Denorm) session.arrays().register(L2Denorm), unconditional
ScalarFnArrayPlugin::new(L2Denorm) removed

VX_SCALAR_FN_ARRAY_TENSOR_PLUGIN no longer gates L2Denorm, since the compressor can emit it. The array ID and L2DenormMetadata are unchanged, so the on-disk bytes match. L2DenormScheme stays out of ALL_SCHEMES.

@connortsui20
connortsui20 force-pushed the ct/l2-denorm-encoding branch from 80daa53 to 63e459b Compare August 2, 2026 18:03
@claude claude Bot added the changelog/break A breaking API change label Aug 2, 2026
@connortsui20
connortsui20 marked this pull request as ready for review August 3, 2026 15:11
@connortsui20

Copy link
Copy Markdown
Member Author

There are things I need to touch up here but would appreciate review on the goal of this PR (making this scalar function an encoding instead)

@connortsui20
connortsui20 force-pushed the ct/l2-denorm-encoding branch 2 times, most recently from d62d2a9 to 8598018 Compare August 3, 2026 15:29
@codspeed-hq

codspeed-hq Bot commented Aug 3, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 10.2%

⚡ 1 improved benchmark
✅ 1841 untouched benchmarks
⏩ 44 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation decode_varbin[(10000, 32)] 154.7 µs 140.4 µs +10.2%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing ct/l2-denorm-encoding (780d4d0) with develop (f79201f)2

Open in CodSpeed

Footnotes

  1. 44 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on develop (3239a5c) during the generation of this report, so f79201f was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Comment on lines +286 to +287
&ConstantArray::new(normalized, 1).into_array(),
&ConstantArray::new(norms, 1).into_array(),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happened to the conversation about having a special class of scalars that are just length-1 arrays?

`L2Denorm` was registered as a `ScalarFnVTable`, but it never behaved like
one. Its constructor took an `ExecutionCtx` and scanned both children to
enforce a unit-norm invariant, `L2Norm` read its stored norms instead of
recomputing, `CosineSimilarity` and `InnerProduct` reached into its physical
children, and the compressor scheme named it as a produced encoding. Those
are all properties of a physical decomposition, not of an operation over
arbitrary well-typed values.

Moves it to `vortex-tensor/src/encodings/l2_denorm/` as a real `VTable` with
two slots (`normalized`, `norms`). Structural validation runs on construction
and on deserialization, `try_new` additionally scans for the exact unit-norm
invariant, and `try_new_trusted` skips that scan for lossy normalized
children whose stored norms stay authoritative. Neither constructor is
`unsafe`, since violating the contract produces wrong answers rather than
undefined behavior.

The encoding keeps the `vortex.tensor.l2_denorm` array ID and the same
two-field metadata message, so the wire format is unchanged.

Slice and filter now push down into both children through `reduce_parent`.
The generic `ScalarFnArray` filter rule only fired when at most one child was
non-constant, which for this encoding was almost never.

Also makes `L2DenormScheme` cascade its two children like `TemporalScheme`
does, which lets both `HACK TO SUPPORT L2 DENORMALIZATION` special cases come
out of `CascadingCompressor`. The scheme now competes on measured size like
every other scheme.

Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
@connortsui20
connortsui20 force-pushed the ct/l2-denorm-encoding branch from 8598018 to 780d4d0 Compare August 3, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog/break A breaking API change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants